package com.ziytek.web.citizen.handler;

import com.ziytek.web.citizen.caffeine.CaffeinePut;
import com.ziytek.web.citizen.config.ConfigInit;
import com.ziytek.web.citizen.config.caffeine.CaffeineUtil;
import com.ziytek.web.citizen.constant.NumConstant;
import com.ziytek.web.citizen.event.OfflineEvent;
import com.ziytek.web.citizen.model.bean.OfflineInfo;
import com.ziytek.web.citizen.service.CabinetStatusPushService;
import com.ziytek.web.citizen.service.ChannelReadService;
import com.ziytek.web.citizen.util.DateTimeUtils;
import com.ziytek.web.citizen.util.SpringBeanUtils;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;

import java.io.IOException;
import java.net.InetSocketAddress;


/**
 * @author fenghx
 * @version v1.0
 * @className BootNettyChannelInboundHandlerAdapter
 * @descripton I/O数据读写处理类
 * @date 2023/4/13 10:27
 **/
@Slf4j
@ChannelHandler.Sharable
public class BootNettyChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter {

    private static final ApplicationContext CONTEXT = SpringBeanUtils.getApplicationContext();
    private static final ConfigInit CONFIG_INIT = SpringBeanUtils.getApplicationContext().getBean(ConfigInit.class);
    private static final CaffeinePut CAFFEINE_PUT = SpringBeanUtils.getApplicationContext().getBean(CaffeinePut.class);
    private static final CabinetStatusPushService PUSH = SpringBeanUtils.getApplicationContext().getBean(CabinetStatusPushService.class);
    private static final ChannelReadService channelReadService = SpringBeanUtils.getApplicationContext().getBean(ChannelReadService.class);

    private static Long SCHEDULED_TIME_DELAY = NumConstant.LONG_ZERO;

    /**
     * TCP客户端上送数据计数缓存
     */
    private void putReadMap(String clientIp, ChannelHandlerContext ctx) {
        if (ctx != null) {
            CaffeineUtil.set(CaffeineUtil.IP_CACHE_PREFIX + clientIp, ctx, NumConstant.INT_SIXTY);
        }
    }

    private Long getReadMap(String clientIp, ChannelHandlerContext ctx) {
        ChannelHandlerContext channelHandlerContext = CaffeineUtil.get(CaffeineUtil.IP_CACHE_PREFIX + clientIp);
        if (ctx != null && channelHandlerContext != null) {
            return getDelayTime();
        }
        return NumConstant.LONG_ZERO;
    }

    private long getDelayTime() {
        if (SCHEDULED_TIME_DELAY <= NumConstant.LONG_ZERO) {
            long scheduledTimeDelay = CONFIG_INIT.getScheduledTimeDelay();
            SCHEDULED_TIME_DELAY = scheduledTimeDelay <= NumConstant.LONG_ZERO ? NumConstant.INT_SIXTY : scheduledTimeDelay;
            return SCHEDULED_TIME_DELAY;
        }
        return SCHEDULED_TIME_DELAY;
    }

    /**
     * 注册时执行
     */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        CaffeineUtil.delete(CaffeineUtil.CACHE_KEY_OFFLINE + getClientIp(ctx));
        super.channelRegistered(ctx);

        CaffeineUtil.set(CaffeineUtil.CACHE_KEY_ONLINE + getClientIp(ctx), System.currentTimeMillis(), 60);
        log.info("电池设备连接注册 Execute during registration--channelRegistered--id:{},client IP:{}", ctx.channel().id().toString(), getClientIp(ctx));
        PUSH.pushByIp(getClientIp(ctx), true);
    }

    /**
     * 离线时执行
     */
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        var ip = getClientIp(ctx);
        var offlineInfo = new OfflineInfo();
        offlineInfo.setOfflineTime(DateTimeUtils.toDate(System.currentTimeMillis(), null));
        offlineInfo.setOfflineReason("client channelUnregistered");
        CaffeineUtil.set(CaffeineUtil.CACHE_KEY_OFFLINE + ip, offlineInfo, 24 * 60 * 60);
        CaffeineUtil.delete(CaffeineUtil.CACHE_KEY_ONLINE + ip);
        PUSH.pushByIp(ip, false);

        CONTEXT.publishEvent(new OfflineEvent(ip));

        super.channelUnregistered(ctx);
        log.info("电池设备离线 offlineReason--channelUnregistered--id:{},client IP:{}", ctx.channel().id().toString(), getClientIp(ctx));
    }

    /**
     * 从客户端收到新的数据时，这个方法会在收到消息时被调用
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        Assert.notNull(msg, "client send msg is null");
        // 在线状态缓存
        CaffeineUtil.set(CaffeineUtil.CACHE_KEY_ONLINE + getClientIp(ctx), System.currentTimeMillis(), 60);

        try {
            channelReadService.channelRead(msg);
        } catch (Exception e) {
            log.info("channelRead error:", e);
        } finally {
            // 解决客户端频繁掉线问题
            if (ctx != null) {
                ctx.flush();
            }
        }
    }

    /**
     * 从客户端收到新的数据、读取完成时调用
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws IOException {
        if (ctx == null) {
            return;
        }
        var clientIp = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress();
        putReadMap(clientIp, ctx);
        // 保存柜子客户端连接IP地址信息缓存
        CAFFEINE_PUT.putChannelContext(clientIp, ctx);
        ctx.flush();
    }

    /**
     * 当出现 Throwable 对象才会被调用，即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        var clientIp = getClientIp(ctx);
        log.info("电池设备通信异常 ip[{}]Netty IO error exceptionCaught: err:", clientIp, cause);

        var offlineInfo = new OfflineInfo();
        offlineInfo.setOfflineTime(DateTimeUtils.toDate(System.currentTimeMillis(), null));
        offlineInfo.setOfflineReason("IO error");
        CaffeineUtil.set(CaffeineUtil.CACHE_KEY_OFFLINE + clientIp, offlineInfo, 24 * 60 * 60);
        CaffeineUtil.delete(CaffeineUtil.CACHE_KEY_ONLINE + clientIp);
        PUSH.pushByIp(clientIp, false);

        //CONTEXT.publishEvent(new OfflineEvent(clientIp));

        // 抛出异常，断开与客户端的连接
        ctx.close();
    }

    /**
     * 客户端与服务端第一次建立连接时 执行
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception, IOException {
        super.channelActive(ctx);
        ctx.channel().read();
        var clientIp = getClientIp(ctx);
        CaffeineUtil.delete(CaffeineUtil.CACHE_KEY_OFFLINE + getClientIp(ctx));
        // 在线状态缓存
        CaffeineUtil.set(CaffeineUtil.CACHE_KEY_ONLINE + getClientIp(ctx), System.currentTimeMillis(), 60);
        // 保存柜子客户端连接IP地址信息缓存
        CAFFEINE_PUT.putChannelContext(clientIp, ctx);
        //此处不能使用ctx.close()，否则客户端始终无法与服务端建立连接
        log.info("电池设备建立连接 When the client and server establish a connection for the first time, channelActive,client IP：{},name:{}", clientIp, ctx.name());
        PUSH.pushByIp(getClientIp(ctx), true);
    }

    /**
     * 客户端与服务端 断连时 执行
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception, IOException {
        super.channelInactive(ctx);
        var clientIp = getClientIp(ctx);
        var idleCount = getReadMap(clientIp, ctx);
        if (idleCount <= NumConstant.LONG_ZERO) {
            long delayTime = getDelayTime();
            OfflineInfo offlineInfo = new OfflineInfo();
            offlineInfo.setOfflineTime(DateTimeUtils.toDate(System.currentTimeMillis(), null));
            offlineInfo.setOfflineReason("Client and server disconnected(No data received from the client)");
            CaffeineUtil.set(CaffeineUtil.CACHE_KEY_OFFLINE + clientIp, offlineInfo, 24 * 60 * 60);
            // 在线状态缓存删除
            CaffeineUtil.delete(CaffeineUtil.CACHE_KEY_ONLINE + clientIp);
            PUSH.pushByIp(clientIp, false);

            CONTEXT.publishEvent(new OfflineEvent(clientIp));
            log.info("电池设备断开连接 Client and server disconnected,No data received from the client within seconds[{}],client IP：{},name:{}", delayTime <= NumConstant.INT_SIXTY ? delayTime : delayTime / NumConstant.INT_ONE_THOUSAND, clientIp, ctx.name());
            ctx.close(); //断开连接时，必须关闭，否则造成资源浪费，并发量很大情况下可能造成宕机
        }
    }

    /**
     * 超时处理 如果规定时间内没有接受客户端的数据,就触发; 如果超过两次，则直接关闭;
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent e && IdleState.READER_IDLE.equals(e.state())) {
            // 如果读通道处于空闲状态，说明没有接收到消息
            var clientIp = getClientIp(ctx);
            var idleCount = getReadMap(clientIp, ctx);
            if (idleCount <= NumConstant.LONG_ZERO) {
                var delayTime = getDelayTime();
                log.info("电池设备超时处理 No client received in seconds[{}]-[{}] idle_count:{}", delayTime <= NumConstant.INT_SIXTY ? delayTime : delayTime / NumConstant.INT_ONE_THOUSAND, clientIp, idleCount);

                var offlineInfo = new OfflineInfo();
                offlineInfo.setOfflineTime(DateTimeUtils.toDate(System.currentTimeMillis(), null));
                offlineInfo.setOfflineReason("timeout");
                CaffeineUtil.set(CaffeineUtil.CACHE_KEY_OFFLINE + clientIp, offlineInfo, 24 * 60 * 60);
                PUSH.pushByIp(clientIp, false);
                // 在线状态缓存删除
                CaffeineUtil.delete(CaffeineUtil.CACHE_KEY_ONLINE + clientIp);

                CONTEXT.publishEvent(new OfflineEvent(clientIp));
                ctx.channel().close();
            }

            return;
        }

        super.userEventTriggered(ctx, evt);
    }

    private String getClientIp(ChannelHandlerContext ctx) {
        return ctx == null ? null : ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress();
    }
}
